//
//  SeizureDetector.m
//  Seizure
//
// Given Accelerometer data, determines if the motion is seizure-like

#import "SeizureDetector.h"
#import "AccelerometerFilter.h"
#import "NSMutableArray+Queue.h"
#import "IntervalInfo.h"
#import "DataManager.h"

//CONSTANTS
#define kUpdateFrequency	60.0
const int INTERVALS_PROCESSED = 3;

@implementation SeizureDetector

@synthesize controllerReceivingUpdates;
@synthesize newDevice;
@synthesize intervalQueue;
@synthesize xcoords;
@synthesize ycoords;
@synthesize zcoords;

#pragma mark -- INITIALIZATION 

//Delegate will have the detectionUpdateWithX: Y: Z: seizureDetected: method
- (id)initWithDelegate:(id<DetectorUpdates>)delegate
{
    self = [super init];
    if (self) {
        //Set the controller delegate that will receive updates on
        //the values of the accelerometer and if a seizure has been detected.
        controllerReceivingUpdates = delegate;
        
        // Setup filter 
        [self changeFilter:[HighpassFilter class]];
        
        // alloc detection queues
        intervalQueue = [[NSMutableArray alloc] init];
        xcoords = [[NSMutableArray alloc] init];
        ycoords = [[NSMutableArray alloc] init];
        zcoords = [[NSMutableArray alloc] init];
        
        //Check the version of the current device
        float version = [[[UIDevice currentDevice] systemVersion] floatValue];
        if (version >= 5.0)
        {
            //iOS 5.0 and later use the CMMotionManager
            newDevice = YES;
            motionManager = [[CMMotionManager alloc] init];
        }
        else
        {
            newDevice = NO;
            //Versions before 5.0 will use the UIAccelDelegate
            [[UIAccelerometer sharedAccelerometer] setUpdateInterval:1.0 / kUpdateFrequency];
            [[UIAccelerometer sharedAccelerometer] setDelegate:self];
        }
    }
    return self;
}

//Changes the filter on the accelerometer
- (void)changeFilter:(Class)filterClass
{
	// Ensure that the new filter class is different from the current one...
	if(filterClass != [filter class])
	{
		// And if it is, release the old one and create a new one.
		filter = nil;
		filter = [[filterClass alloc] initWithSampleRate:kUpdateFrequency cutoffFrequency:5.0];
		// Set the adaptive flag
		filter.adaptive = YES;
	}
}

//Change the filter sensitivity
- (void)setFilterSensitivity:(double)sensitivity
{
    [filter setSensitivity:sensitivity];
}

#pragma mark -- Seizure Detection

/**
 *  Returns YES if a seizure has been detected.
 */
-(BOOL)detectSeizure
{
    BOOL result = NO;
    static NSDate *lastTime = nil;
    if (lastTime == nil){
        lastTime = [NSDate dateWithTimeIntervalSinceNow:0];
    }
    
    // one second has passed
    if ([lastTime timeIntervalSinceNow] <= -1.0)
    {
        lastTime = nil;
        
        // get characteristics for this time interval
        IntervalInfo *info = [[IntervalInfo alloc] initWithIntervalDataForX:xcoords forY:ycoords forZ:zcoords];
        [intervalQueue enqueue:info];
        if ([intervalQueue count] > INTERVALS_PROCESSED){
            [intervalQueue dequeue];
        }
        if ([intervalQueue count] == INTERVALS_PROCESSED){
            result = [self analyzeIntervals:intervalQueue];
            if (result)
            {
                // seizure detected, clear interval queue
                [intervalQueue removeAllObjects];
            }
        }
        
        // start fresh second
        [xcoords removeAllObjects];
        [ycoords removeAllObjects];
        [zcoords removeAllObjects];
    }
    // absolute value
    if (fabs(filter.x) > 0.001) 
        [xcoords enqueue:[[NSNumber alloc] initWithDouble:filter.x]];
    if (fabs(filter.y) > 0.001)
        [ycoords enqueue:[[NSNumber alloc] initWithDouble:filter.y]];
    if (fabs(filter.z) > 0.001)
        [zcoords enqueue:[[NSNumber alloc] initWithDouble:filter.z]];
    
    return result;
}

// Analyze the freqency between intervals, return true if freqences are similar
-(BOOL)algo2:(NSMutableArray*)intervals
{    
    const static double VARIANCE_THRESHOLD2 = 2.5; // highest allowance of variablitiy of axis frequency

    double overallAverageX = 0.0;
    double overallAverageY = 0.0;
    double overallAverageZ = 0.0;
    
    double overallSqrAverageX = 0.0;
    double overallSqrAverageY = 0.0;
    double overallSqrAverageZ = 0.0;
    
    double freqX;
    double freqY;
    double freqZ;
    
    for (int i=0; i<[intervals count]; i++)
    {
        IntervalInfo *interval = [intervals objectAtIndex:i];
        
        freqX = [interval freqX];
        freqY = [interval freqY];
        freqZ = [interval freqZ];
        
        overallAverageX += freqX;
        overallSqrAverageX += freqX * freqX;
        
        overallAverageY += freqY;
        overallSqrAverageY += freqY * freqY;
        
        overallAverageZ += freqZ;
        overallSqrAverageZ += freqZ * freqZ;
    }
    double meanX = overallAverageX / INTERVALS_PROCESSED;
    double meanY = overallAverageY / INTERVALS_PROCESSED;
    double meanZ = overallAverageZ / INTERVALS_PROCESSED;
    
    double varianceX = (overallSqrAverageX - overallAverageX*meanX) / (INTERVALS_PROCESSED - 1);
    double varianceY = (overallSqrAverageY - overallAverageY*meanY) / (INTERVALS_PROCESSED - 1);
    double varianceZ = (overallSqrAverageZ - overallAverageZ*meanZ) / (INTERVALS_PROCESSED - 1);
//    NSLog(@"Freq Variance X -> %f, Freq Variance Y -> %f, Freq Variance Z -> %f",varianceX, varianceY,varianceZ);
    BOOL algo2 = NO;
    
    if (varianceX < VARIANCE_THRESHOLD2 ||
        varianceY < VARIANCE_THRESHOLD2 ||
        varianceZ < VARIANCE_THRESHOLD2)
    {
        algo2 = YES;
    }
    return algo2;
}


-(BOOL)analyzeIntervals:(NSMutableArray*)intervals
{
    DataManager *dm = [DataManager sharedInstance];
    NSNumber *s = [dm getStoredAccount].sensitivity;
//    double threshold = 0.16;
    double threshold = 0.14;//3 + (0.0002*s.integerValue);
    
    // variance for >= 70 sensitivity
    static double VARIANCE_THRESHOLD = 0.035;

    if (s.integerValue < 30)
        VARIANCE_THRESHOLD = 0.006;
    else if (s.integerValue < 70)
        VARIANCE_THRESHOLD = 0.008;
    
    
    double overallAverageX = 0.0;
    double overallAverageY = 0.0;
    double overallAverageZ = 0.0;
    
    double overallSqrAverageX = 0.0;
    double overallSqrAverageY = 0.0;
    double overallSqrAverageZ = 0.0;
    
    int aboveThresholdX = 0;
    int aboveThresholdY = 0;
    int aboveThresholdZ = 0;
    // get the average of max values
    for (int i=0; i<[intervals count]; i++)
    {
        double avgX = [(IntervalInfo *)[intervals objectAtIndex:i] averageMaxValuesX];
        double avgY = [(IntervalInfo *)[intervals objectAtIndex:i] averageMaxValuesY];
        double avgZ = [(IntervalInfo *)[intervals objectAtIndex:i] averageMaxValuesZ];
        
        double varianceX = [(IntervalInfo *)[intervals objectAtIndex:i] varianceX];
        double varianceY = [(IntervalInfo *)[intervals objectAtIndex:i] varianceY];
        double varianceZ = [(IntervalInfo *)[intervals objectAtIndex:i] varianceZ];
//        NSLog(@"Variance Interval %d ---- ", i);
//        NSLog(@"Variance X -> %f", varianceX);
//        NSLog(@"Variance Y -> %f", varianceY);
//        NSLog(@"Variance Z -> %f", varianceZ);
        if (varianceX > VARIANCE_THRESHOLD)
        {
            // pattern within interval was not uniform enough
            aboveThresholdX--;
        }
        if (varianceY > VARIANCE_THRESHOLD)
        {
            // pattern within interval was not uniform enough
            aboveThresholdY--;
        }
        if (varianceZ > VARIANCE_THRESHOLD)
        {
            // pattern within interval was not uniform enough
            aboveThresholdZ--;
        }
        
        ////////////////////////////////////
        
        // count the num of passed threasholds, needs to be num of processed
        if (avgX > threshold)
            aboveThresholdX++;
        if (avgY > threshold)
            aboveThresholdY++;
        if (avgZ > threshold)
            aboveThresholdZ++;
        
        overallAverageX += avgX;
        overallAverageY += avgY;
        overallAverageZ += avgZ;
        
        overallSqrAverageX += avgX * avgX;
        overallSqrAverageY += avgY * avgY;
        overallSqrAverageZ += avgZ * avgZ;
    }
    double meanX = overallAverageX / INTERVALS_PROCESSED;
    double meanY = overallAverageY / INTERVALS_PROCESSED;
    double meanZ = overallAverageZ / INTERVALS_PROCESSED;
    
    double varianceX = (overallSqrAverageX - overallAverageX*meanX) / (INTERVALS_PROCESSED - 1);
    double varianceY = (overallSqrAverageY - overallAverageY*meanY) / (INTERVALS_PROCESSED - 1);
    double varianceZ = (overallSqrAverageZ - overallAverageZ*meanZ) / (INTERVALS_PROCESSED - 1);
//    NSLog(@"----");
//    NSLog(@"Variance of max X was %f", varianceX);
//    NSLog(@"Variance of max Y was %f",varianceY);
//    NSLog(@"Variance of max Z was %f",varianceZ);
//    NSLog(@"|");
    
    BOOL algo1 = NO;
    
    // only if all INTERVALS were above the threshold
    if (aboveThresholdX == INTERVALS_PROCESSED-1)
    {
        if (varianceX < VARIANCE_THRESHOLD)
            algo1 = YES;
    }
    if (aboveThresholdY == INTERVALS_PROCESSED-1)
    {
        if (varianceY < VARIANCE_THRESHOLD){
            algo1 = YES;
        }
    }
    if (aboveThresholdZ == INTERVALS_PROCESSED-1)
    {
        if (varianceZ < VARIANCE_THRESHOLD)
            algo1 = YES;
    }
    
    BOOL result = [self algo2:intervals] && algo1;
    
    return result;
}


#pragma mark -- Accelerometer Management

- (void)startGettingData
{
    if (newDevice)
    {
        [motionManager startAccelerometerUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:
         ^(CMAccelerometerData *accelerometerData, NSError *error){
             
             dispatch_async(dispatch_get_main_queue(), ^{
                 
                 [filter addAccelerationX:accelerometerData.acceleration.x
                                        y:accelerometerData.acceleration.y 
                                        z:accelerometerData.acceleration.z];
                 BOOL detected = [self detectSeizure];
                 
                 //Tell the delegate the current x,y,z values from accelerometer and
                 //if a seizure was detected yet.
                 [controllerReceivingUpdates detectionUpdateWithX:filter.x Y:filter.y Z:filter.z seizureDetected:detected];
             });
         }];
    }
    else
    {
        // older device is already setup
        [[UIAccelerometer sharedAccelerometer] setDelegate:self];
    }
    
}

- (void)stopGettingData
{
    if( newDevice )
    {
        [motionManager stopAccelerometerUpdates];
    }
    else
    {
        [[UIAccelerometer sharedAccelerometer] setDelegate:nil];
    }
}


#pragma mark UIAccelDelegate

// UIAccelerometerDelegate method, called when the device accelerates.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
        // Update the accelerometer graph view
    [filter addAccelerationX:acceleration.x
                           y:acceleration.y 
                           z:acceleration.z];
    BOOL detected = [self detectSeizure];
    [controllerReceivingUpdates detectionUpdateWithX:filter.x Y:filter.y Z:filter.z seizureDetected:detected];
}

@end
